# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""
This is the main WSGI handler file for Apache Kibble.
It compiles a list of valid URLs from the 'pages' library folder,
and if a URL matches it runs the specific submodule's run() function. It
also handles CGI parsing and exceptions in the applications.
"""
import os
import re
import sys
import traceback
import yaml
import json

from kibble.api.plugins import openapi
from kibble.api.plugins.database import KibbleDatabase
from kibble.api.plugins.session import KibbleSession


# Compile valid API URLs from the pages library
# Allow backwards compatibility by also accepting .lua URLs
from kibble.settings import KIBBLE_YAML, YAML_DIRECTORY

urls = []
if __name__ != "__main__":
    from kibble.api.pages import handlers

    for page, handler in handlers.items():
        urls.append((r"^(/api/%s)(/.+)?$" % page, handler.run))


# Load Kibble master configuration
with open(KIBBLE_YAML, "r") as f:
    config = yaml.load(f)

# Instantiate database connections
DB = None

# Load Open API specifications
openapi_yaml = os.path.join(YAML_DIRECTORY, "openapi.yaml")
KibbleOpenAPI = openapi.OpenAPI(openapi_yaml)


class KibbleHTTPError(Exception):
    def __init__(self, code, message):
        self.code = code
        self.message = message


class KibbleAPIWrapper:
    """
    Middleware wrapper for exceptions in the application
    """

    def __init__(self, path, func):
        self.func = func
        self.API = KibbleOpenAPI
        self.path = path
        self.exception = KibbleHTTPError

    def __call__(self, environ, start_response, session):
        """Run the function, return response OR return stacktrace"""
        try:
            # Read JSON client data if any
            try:
                request_size = int(environ.get("CONTENT_LENGTH", 0))
            except (ValueError):
                request_size = 0
            requestBody = environ["wsgi.input"].read(request_size)
            formdata = {}
            if requestBody and len(requestBody) > 0:
                try:
                    formdata = json.loads(requestBody.decode("utf-8"))
                except json.JSONDecodeError as err:
                    start_response(
                        "400 Invalid request", [("Content-Type", "application/json")]
                    )
                    yield json.dumps({"code": 400, "reason": "Invalid JSON: %s" % err})
                    return

            # Validate URL against OpenAPI specs
            try:
                self.API.validate(environ["REQUEST_METHOD"], self.path, formdata)
            except openapi.OpenAPIException as err:
                start_response(
                    "400 Invalid request", [("Content-Type", "application/json")]
                )
                yield json.dumps({"code": 400, "reason": err.message})
                return

            # Call page with env, SR and form data
            try:
                response = self.func(self, environ, formdata, session)
                if response:
                    for bucket in response:
                        yield bucket
            except KibbleHTTPError as err:
                errHeaders = {
                    403: "403 Authentication failed",
                    404: "404 Resource not found",
                    500: "500 Internal Server Error",
                    501: "501 Gateway error",
                }
                errHeader = (
                    errHeaders[err.code]
                    if err.code in errHeaders
                    else "400 Bad request"
                )
                start_response(errHeader, [("Content-Type", "application/json")])
                yield json.dumps(
                    {"code": err.code, "reason": err.message}, indent=4
                ) + "\n"
                return

        except:
            err_type, err_value, tb = sys.exc_info()
            traceback_output = ["API traceback:"]
            traceback_output += traceback.format_tb(tb)
            traceback_output.append("%s: %s" % (err_type.__name__, err_value))
            # We don't know if response has been given yet, try giving one, fail gracefully.
            try:
                start_response(
                    "500 Internal Server Error", [("Content-Type", "application/json")]
                )
            except:
                pass
            yield json.dumps({"code": "500", "reason": "\n".join(traceback_output)})


def fourohfour(environ, start_response):
    """A very simple 404 handler"""
    start_response("404 Not Found", [("Content-Type", "application/json")])
    yield json.dumps({"code": 404, "reason": "API endpoint not found"}, indent=4) + "\n"
    return


def application(environ, start_response):
    """
    This is the main handler. Every API call goes through here.
    Checks against the pages library, and if submod found, runs
    it and returns the output.
    """
    db = KibbleDatabase(config)
    path = environ.get("PATH_INFO", "")
    for regex, function in urls:
        m = re.match(regex, path)
        if m:
            callback = KibbleAPIWrapper(path, function)
            session = KibbleSession(db, environ, config)
            a = 0
            for bucket in callback(environ, start_response, session):
                if a == 0:
                    session.headers.append(bucket)
                    try:
                        start_response("200 Okay", session.headers)
                    except:
                        pass
                a += 1
                # WSGI prefers byte strings, so convert if regular py3 string
                if isinstance(bucket, str):
                    yield bytes(bucket, encoding="utf-8")
                elif isinstance(bucket, bytes):
                    yield bucket
            return

    for bucket in fourohfour(environ, start_response):
        yield bytes(bucket, encoding="utf-8")


if __name__ == "__main__":
    KibbleOpenAPI.toHTML()
